Skip to content

fix: resolve flash of unstyled content in dark mode#766

Open
TusharThakur04 wants to merge 1 commit intonodejs:mainfrom
TusharThakur04:fix/dark-mode-FOUC
Open

fix: resolve flash of unstyled content in dark mode#766
TusharThakur04 wants to merge 1 commit intonodejs:mainfrom
TusharThakur04:fix/dark-mode-FOUC

Conversation

@TusharThakur04
Copy link
Copy Markdown
Contributor

Description

This PR resolves the FOUC by resolving system to dark or light using matchMedia.
Earlier system was directly being applied as set-theme = system

Validation

before :

Screencast.from.2026-04-13.19-26-34.webm

after :

Screencast.from.2026-04-13.19-24-03.webm

Related Issues

n/a

Check List

  • I have read the Contributing Guidelines and made commit messages that follow the guideline.
  • I have run node --run test and all tests passed.
  • I have check code formatting with node --run format & node --run lint.
  • I've covered new added functionality with unit tests if necessary.

@TusharThakur04 TusharThakur04 requested a review from a team as a code owner April 13, 2026 14:23
@cursor
Copy link
Copy Markdown

cursor Bot commented Apr 13, 2026

PR Summary

Low Risk
Low risk: changes are limited to how the initial data-theme is computed and inlined before first paint; main risk is edge-case behavior differences for users with stored theme preferences.

Overview
Fixes initial-page theme application to avoid dark-mode Flash of Unstyled Content by replacing the hardcoded inline snippet with an injected themeScript.

Adds ui/theme-script.mjs, which resolves the stored theme preference (including system) to a concrete dark/light value via matchMedia and applies it to documentElement before render; processing.mjs now passes this script into template.html for inlining.

Reviewed by Cursor Bugbot for commit ae47eff. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
api-docs-tooling Ready Ready Preview Apr 25, 2026 5:43pm

Request Review

Comment thread src/generators/web/template.html Outdated
Comment thread src/generators/web/template.html Outdated

<!-- Apply theme before paint to avoid Flash of Unstyled Content -->
<script>document.documentElement.setAttribute("data-theme", document.documentElement.style.colorScheme = localStorage.getItem("theme") || (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"));</script>
<script>(function(){var p=localStorage.getItem("theme");var t=(!p||p==="system")?(matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"):p;document.documentElement.setAttribute("data-theme",t);document.documentElement.style.colorScheme=t;})();</script>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should make this code block more readable for developers who want to contribute or review it. Instead of writing it as a single line, using more descriptive variable names shouldn't significantly impact the file size. @nodejs/web-infra WDYT?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If that's the case this script source should be in its own file and then inlined via a template string {{}}

@avivkeller
Copy link
Copy Markdown
Member

This seems highly complex

@TusharThakur04
Copy link
Copy Markdown
Contributor Author

the {$themeScript} literal is being replaced by newly created themeScript by generate.mjs

it is done so that the script is available before rendering the html and there is no latency which could have happened if browser makes request for the themeScript.mjs if we use src inside the script tag

Comment thread src/generators/web/generate.mjs Outdated

// Wrap theme script in script tag and replace the placeholder
const inlinedThemeScript = `<script>${themeScriptContent}</script>`;
template = template.replace('${themeScript}', inlinedThemeScript);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use .replace(), I believe that @avivkeller implemented a system to automatically evaluate such template variables?

Comment thread src/generators/web/template.html Outdated
It injects an inline script that initializes the theme (light/dark)
before the page renders to avoid Flash of Unstyled Content (FOUC).
-->
${themeScript}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
${themeScript}
<script type="javascript">${themeScript}</script>

Comment thread src/generators/web/template.html Outdated

<!-- Apply theme before paint to avoid Flash of Unstyled Content -->
<script>document.documentElement.setAttribute("data-theme", document.documentElement.style.colorScheme = localStorage.getItem("theme") || (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"));</script>
<!--
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for this comment.

Comment thread src/generators/web/generate.mjs Outdated

// Read the theme script from UI folder
const themeScriptPath = join(import.meta.dirname, 'ui', 'theme-script.mjs');
const themeScriptContent = await readFile(themeScriptPath, 'utf-8');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't ue fs the idea is to use our rolldown pipeline for this script, so it gets properly minified and transformed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise the file doesn't get minified, which is the idea behind separating src<>final result.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 18, 2026

Codecov Report

❌ Patch coverage is 50.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 77.94%. Comparing base (3d00fa8) to head (ae47eff).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
src/generators/web/utils/processing.mjs 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #766      +/-   ##
==========================================
- Coverage   78.43%   77.94%   -0.49%     
==========================================
  Files         157      159       +2     
  Lines       13962    14058      +96     
  Branches     1152     1152              
==========================================
+ Hits        10951    10958       +7     
- Misses       3006     3095      +89     
  Partials        5        5              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread src/generators/web/utils/processing.mjs Outdated
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3c1f1be. Configure here.

Comment thread src/generators/web/ui/theme-script.mjs Outdated
@TusharThakur04
Copy link
Copy Markdown
Contributor Author

@ovflowd
please review, i ve made the changes you suggested

Comment thread src/generators/web/ui/theme-script.mjs Outdated
Comment on lines +1 to +25
'use strict';

/**
* This script is designed to be inlined in the <head> of the HTML template.
* It must execute BEFORE the body renders to prevent a Flash of Unstyled Content (FOUC).
*/

export default `(function initializeTheme() {
const THEME_STORAGE_KEY = 'theme';
const THEME_DATA_ATTRIBUTE = 'data-theme';
const DARK_QUERY = '(prefers-color-scheme: dark)';

const savedUserPreference = localStorage.getItem(THEME_STORAGE_KEY);
const systemSupportsDarkMode = window.matchMedia(DARK_QUERY).matches;

const shouldApplyDark =
savedUserPreference === 'dark' ||
(savedUserPreference === 'system' && systemSupportsDarkMode) ||
(!savedUserPreference && systemSupportsDarkMode);

const themeToApply = shouldApplyDark ? 'dark' : 'light';

document.documentElement.setAttribute(THEME_DATA_ATTRIBUTE, themeToApply);
document.documentElement.style.colorScheme = themeToApply;
})();`.trim();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do go with the "custom script file" way, we should import with fs and not a string

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread src/generators/web/ui/theme-script.mjs Outdated
Comment thread src/generators/web/ui/theme-script.mjs Outdated
* It must execute BEFORE the body renders to prevent a Flash of Unstyled Content (FOUC).
*/
function initializeTheme() {
const THEME_STORAGE_KEY = 'theme';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though const is widely available, since this scripts will run directly in the browser, it’s safer to keep it as var

Copy link
Copy Markdown
Member

@canerakdas canerakdas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, I just left one comment 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants